# irc.rb - IRC protocol class
# Copyright (C) 2005-2009 Akira TAGOH

# Authors:
#   Akira TAGOH  <akira@tagoh.org>

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.

require 'iconv'
require 'prune/charset'
require 'prune/debug'
require 'prune/event'
require 'prune/parser'
require 'prune/socket'
require 'prune/state'
require 'prune/plugin'


Thread.abort_on_exception = true if $DEBUG
PRUNE::Parser.ignore_invalid_message = false if $DEBUG
PRUNE::Message.check_params = :strict if $DEBUG


module PRUNE

=begin rdoc

== PRUNE::IRC

=end

  class IRC
    include PRUNE::Debug

    MODE_CONN = 0
    MODE_LISTEN = 1

=begin rdoc

=== PRUNE::IRC#new

=end

    def initialize
      @parser = PRUNE::Parser.new
      @socket = nil
      @sdelegate = nil
      @socketmgr = PRUNE::SocketManager.instance
      @eventmgr = PRUNE::EventManager.instance
      @pluginmgr = PRUNE::PluginManager.instance
      @charset = PRUNE::Charset.new
      @charset.default = 'UTF-8'
      @state = PRUNE::IRCState.new
      @ids = {}
      @mode = nil
      @value = {}

      @abort_on_parser_exception = false
    end # def initialize

    attr_accessor :abort_on_parser_exception, :charset
    attr_reader :state

=begin rdoc

=== PRUNE::IRC#inspect

=end

    def inspect
      cat = self.category_list
      if $DEBUG && cat.list.include?('irc/debug') then
        
      else
        retval = sprintf("#<%s:0x%x value:%s>",
                         self.class, self.object_id, @value.inspect)
      end

      return retval
    end # def inspect

=begin rdoc

=== PRUNE::IRC#open(socket, mode = MODE_CONN, options = {})

=end

    def open(socket, key, mode = MODE_CONN, options = {})
      return false unless socket.kind_of?(PRUNE::SocketDelegator)
      PRUNE.Fail(PRUNE::Error::AlreadyOpened) if mode == MODE_CONN && @state.connected? || mode == MODE_LISTEN && @state.listen?

      @mode = mode
      @ids = @eventmgr.auto_registration(key, self, '', PRUNE::EventManager::PRIOR_SYS_SYNC_HIGH)

      copts = {}
      if options.has_key?('Channels') && options['Channels'].kind_of?(Array) then
        options['Channels'].each do |x|
          copts[x[0]] = x[1].dup unless x[1].nil?
        end
      end

      @value[:loaded_plugins] = {} unless @value.has_key?(:loaded_plugins)
      @value[:loaded_plugins][key] = []
      if options.has_key?('Plugins') && options['Plugins'].kind_of?(Array) then
        options['Plugins'].each do |file, opts|
          if @pluginmgr.load_plugin(file, key, copts, opts) then
            @value[:loaded_plugins][key] << file
          end
        end
      end

      @socket = socket
      @socketmgr.readq << socket
      @socketmgr.start
    end # def open

=begin rdoc

=== PRUNE::IRC#close

=end

    def close
      return false if @state.nil?

      Thread.exclusive do
        if @mode == MODE_CONN then
          @state.connected = false
        else
          @state.listened = false
        end
        info("Unloading the plugins...")
        @value[:loaded_plugins][@sdelegate.key].each do |f|
          @pluginmgr.unload_plugin(f, @sdelegate.key)
        end
        info("Stopping the event handlers...")
        @eventmgr.auto_unregistration(@ids)
        info("Closing socket...")
        @socket.close if !@socket.nil? && @socket.closed?
        info("Closed socket")
      end

      true
    end # def close

=begin rdoc

=== PRUNE::IRC#closed?

=end

    def closed?
      @socket.nil? || @socket.closed?
    end # def closed?

=begin rdoc

=== PRUNE::IRC#convert_utf8_to(msg)

=end

    def convert_to_utf8(msg) #:yields: msg
      unless msg.channel.nil? then
        charsets = []
        channels = msg.channel({:suffix=>true, :array=>true})
        channels.each do |ch|
          charsets << @charset[ch]
        end
        unless msg.enforced_charset.nil? then
          charsets = [msg.enforced_charset]
        end
        if charsets.uniq.length > 1 then
          # has to be a separate message
          ci = PRUNE::Message.channelinfo(msg.command)
          channels.each do |ch|
            case ci.location
            when PRUNE::Message::CH_PARAM
              msg.params[ci.index] = ch
            when PRUNE::Message::CH_TRAILING
              msg.params[-1] = ch
            end
            yield _convert_to_utf8(msg, @charset[ch])
          end
        else
          yield _convert_to_utf8(msg, charsets[0])
        end
      else
        yield msg
      end
    end # def convert_to_utf8

=begin rdoc

=== PRUNE::IRC#convert_from_utf8(msg) #:yields: msg

=end

    def convert_from_utf8(msg)
      unless msg.channel.nil? then
        charsets = []
        channels = msg.channel({:suffix=>true, :array=>true})
        channels.each do |ch|
          charsets << @charset[ch]
        end
        unless msg.enforced_charset.nil? then
          charsets = [msg.enforced_charset]
        end
        if charsets.uniq.length > 1 then
          # has to be a separate message
          ci = PRUNE::Message.channelinfo(msg.command)
          channels.each do |ch|
            case ci.location
            when PRUNE::Message::CH_PARAM
              msg.params[ci.index] = ch
            when PRUNE::Message::CH_TRAILING
              msg.params[-1] = ch
            end
            yield _convert_from_utf8(msg, @charset[ch])
          end
        else
          yield _convert_from_utf8(msg, charsets[0])
        end
      else
        yield msg
      end
    end # def convert_from_utf8

=begin rdoc

=== PRUNE::IRC#register(signal, priority, instance, function, *args)

=end

    def register(signal, priority, instance, function, *args)
      @eventmgr.register(@sdelegate.key, signal, priority, instance, function, *args)
    end # def register

=begin rdoc

=== PRUNE::IRC#unregister(handler_id)

=end

    def unregister(handler_id)
      @eventmgr.unregister(handler_id)
    end # def unregister

=begin rdoc

=== PRUNE::IRC#synchronize(mode = true)

=end

    def synchronize(mode = true)
      @eventmgr.synchronize(mode) do |i|
        yield self
      end
    end # def synchronize

=begin rdoc

=== PRUNE::IRC#emit(target, signal, subsignal, *data)

=end

    def emit(target, signal, subsignal, *data)
      if signal == :Received || signal == :Sent then
        emit_message(target, signal, subsignal, *data)
      else
        @eventmgr.emit(target.nil? ? @sdelegate.key : target, signal, subsignal, @state, *data)
      end
    end # def emit

=begin rdoc

=== PRUNE::IRC#emit_message(target, signal, subsignal, message)

=end

    def emit_message(target, signal, subsignal, message)
      @eventmgr.emit(target.nil? ? @sdelegate.key : target, signal, subsignal == true ? message.command : nil, @state, message)
    end # def emit_message

=begin rdoc

=== PRUNE::IRC#stop_emission(handler_id)

=end

    def stop_emission(handler_id)
      @eventmgr.stop_emission(handler_id)
    end # def stop_emission

    private

    def _convert_to_utf8(msg, charset)
      if charset.downcase != 'utf-8' && charset.downcase != 'utf8' then
        begin
          debug('iconv', "Converting `%s' from %s to UTF-8", msg.to_s, charset)
          ll = Iconv.conv('UTF-8', charset, msg.to_s)
          msg = @parser.parse(ll)
        rescue Iconv::IllegalSequence
          warning("Error occured while converting `%s' from %s to UTF-8", msg.to_s, charset)
        rescue PRUNE::Error::UnknownToken => e
          bug("Unknown token `%s' was found.", ll.chomp("\r\n"))
          raise if @abort_on_parser_exception
        rescue => e
          bug("Unexpected exception: %s", e.message)
          raise if @abort_on_parser_exception
        end
      end

      msg
    end # def _convert_to_utf8

    def _convert_from_utf8(msg, charset)
      if charset.downcase != 'utf-8' && charset.downcase != 'utf8' then
        begin
          debug('iconv', "Converting `%s' from UTF-8 to %s", msg.to_s, charset)
          ll = Iconv.conv(charset, 'UTF-8', msg.to_s)
          msg = @parser.parse(ll)
        rescue Iconv::IllegalSequence
          warning("Error occured while converting `%s' from UTF-8 to %s", msg.to_s, charset)
        rescue PRUNE::Error::UnknownToken => e
          bug("Unknown token `%s' was found.", ll.chomp("\r\n"))
          raise if @abort_on_parser_exception
        rescue => e
          bug("Unexpected exception: %s", e.message)
          raise if @abort_on_parser_exception
        end
      end

      msg
    end # def _convert_from_utf8

    protected

    def _async(ret, key, *args)
      if @mode == MODE_CONN && @state.loggedin? then
        t = Time.now

        unless @value.has_key?(:ping) then
          @value[:ping] = PRUNE::TYPE::PingInfoStruct.new(0, t - 301, nil)
        end
        if @value[:ping].pinged_time.kind_of?(Time) then
          if t - @value[:ping].pinged_time > 180 then
            @eventmgr.emit(key, :Received, nil,
                           PRUNE::Message::ERROR.new(sprintf("Ping timeout: %d seconds", t - @value[:ping].pinged_time)).to_s)
          end
        else
          if t - @value[:ping].timer > 300 then
            msg = PRUNE::Message::PING.new(@state.nick)
            ret.emit_message(key, :Sent, false, msg)
            @value[:ping].pinged_time = t
            @value[:ping].signature = @state.nick.dup
          end
        end
      end

      false
    end # def _async

    def _connected(ret, key, *args)
      info("Connected to `%s'.", key)
      if @mode == MODE_CONN then
        @state.connected = true
        begin
          @state.host = args[0].addr[2]
        rescue IOError => e
          warning("Error occurred during processing a Connected signal: %s", e.message)
        end
      end

      false
    end # def _connected

    def _disconnected(ret, key, *args)
      info("Disconnected from `%s'.", key)
      if @mode == MODE_CONN then
        @socket = nil
        @state.connected = false
      end

      false
    end # def _disconnected

    def _received(ret, key, *args)
      str = args[0]

      return false unless str.kind_of?(String)

      msg = nil
      begin
        msg = @parser.parse(str)
      rescue Racc::ParseError => e
        warning("Can't parse `%s'", str.chomp("\r\n"))
        raise if @abort_on_parser_exception
        if PRUNE::Parser.ignore_invalid_message then
          msg = PRUNE::Message::RAW.new(str)
        else
          msg = nil
        end
      rescue PRUNE::Error::InvalidMessage => e
        raise if @abort_on_parser_exception
      rescue PRUNE::Error::UnknownToken => e
        bug("Unknown token was found in %s", str.chomp("\r\n"))
        raise if @abort_on_parser_exception
        if PRUNE::Parser.ignore_invalid_message then
          msg = PRUNE::Message::RAW.new(str)
        else
          msg = nil
        end
      rescue => e
        bug("Unexpected exception: %s", e.message)
        raise if @abort_on_parser_exception
        if PRUNE::Parser.ignore_invalid_message then
          msg = PRUNE::Message::RAW.new(str)
        else
          msg = nil
        end
      end
      unless msg.nil? then
        convert_to_utf8(msg) do |m|
          v = nil
          @eventmgr.synchronize do
            v = ret.emit_message(key, :Received, true, m)
          end
          unless v[0] then
            ret.emit_message(key, :Received, false, m)
          end
        end
      end

      true
    end # def _received

  end # class IRC

=begin rdoc

== PRUNE::IRCConnector

=end

  class IRCConnector < PRUNE::IRC

=begin rdoc

=== PRUNE::IRCConnector#new

=end

    def initialize
      super

      @reconnection_timer = 30
      @sigids = {}
      @conninfo = nil
    end # def initialize

=begin rdoc

=== PRUNE::IRCConnector#open(conninfo, key = nil)

=end

    def open(conninfo, key = nil)
      ca = []
      if conninfo.kind_of?(Array) then
        ca = conninfo
      else
        ca << conninfo
      end
      marker = ca[0]
      begin
        ci = ca.shift
        ca << ci
        info("Connecting to %s/%s...", ci.host, ci.port)
        socket = PRUNE::TCPSocket.new(ci.host, ci.port)
        @conninfo = ci
        @sdelegate = PRUNE::SocketDelegator.new(self, socket, key)
      rescue IOError, SocketError, Errno::EPIPE, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EHOSTUNREACH
        @conninfo = nil
        if ca[0] == marker then
          info("Failed to connect to the IRC server and no alternatives to fall back: %s/%s", ci.host, ci.port)
          return false
        end
        info("Unable to connect to %s/%s. will try after %d sec.", ci.host, ci.port, @reconnection_timer)
        sleep @reconnection_timer
        retry
      end

      @state.nick = @conninfo.nick
      @state.user = @conninfo.user
      @state.name = @conninfo.name

      if @conninfo.options.has_key?('Charset') then
        @charset.default = @conninfo.options['Charset']
      end
      if @conninfo.options.has_key?('Channels') && @conninfo.options['Channels'].kind_of?(Array) then
        @conninfo.options['Channels'].each do |x|
          if x[1].kind_of?(Hash) && x[1].has_key?('Charset') then
            @charset[x[0]] = x[1]['Charset']
          end
        end
      end

      super(@sdelegate, @sdelegate.key, PRUNE::IRC::MODE_CONN, @conninfo.options)

      emit(nil, :Connected, nil, @socket)

      @sigids = @eventmgr.auto_registration(@sdelegate.key, self, '_conn', PRUNE::EventManager::PRIOR_SYS_SYNC_HIGH)
      if block_given? then
        until @sdelegate.closed? do
          if @state.connected? then
            @eventmgr.emit(@sdelegate.key, :Async, nil, @state)
            yield @eventmgr, @state, @sdelegate.key
          else
            # nothing to do
          end
          sleep 1
        end
        close
        true
      else
        return @sdelegate.key
      end
    end # def open

=begin rdoc

=== PRUNE::IRCConnector#close

=end

    def close
      return false unless @state.connected?
      Thread.exclusive do
        info("Stopping the event handlers for connector...")
        @eventmgr.auto_unregistration(@sigids)
        super
      end
    end # def close

    protected

    def _conn_sent(ret, key, *args)
      msg = args[0]

      if msg.nick.nil? && msg.user.nil? && msg.host.nil? then
        msg.nick = @state.nick
        msg.user = @state.user
        msg.host = @state.host
      end

      ret.synchronize do |x|
        x.emit_message(key, :Sent, true, msg)
      end

      convert_from_utf8(msg) do |m|
        begin
          @socket.puts(m.to_s)
        rescue IOError, SocketError, Errno::PIPE, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EHOSTUNREACH
          ret.emit(key, :Disconnected, nil, @socket)
        end
      end

      false
    end # def _conn_sent

    def _conn_connected(ret, key, *args)
      q = []
      if @conninfo.options.has_key?('Password') && !@conninfo.options['Password'].nil? then
        q << PRUNE::Message::PASS.new(@conninfo.options['Password'], nil)
      end
      q << PRUNE::Message::NICK.new(@state.nick)
      q << PRUNE::Message::USER.new(@state.user, '*', '*', @state.name)
      info("Logging into the IRC server %s/%s", @conninfo.host, @conninfo.port)
      q.each do |m|
        ret.emit_message(key, :Sent, false, m)
      end

      false
    end # def _conn_connected

    def _conn_loggedin(ret, key, *args)
      @state.loggedin = true

      false
    end # def _conn_loggedin

    def _conn_error_received(ret, key, *args)
      unless @socket.closed? then
        close
      end

      false
    end # def _conn_error_received

    def _conn_join_received(ret, key, *args)
      msg = args[0]

      if msg.channel.nil? then
        bug("Empty channel at JOIN: %s", msg)
      elsif msg.nick.nil? then
        bug("Empty nick in message at JOIN: %s", msg)
      else
        @state.join(msg.channel, msg.nick)
      end

      false
    end # def _conn_join_received

    def _conn_kick_received(ret, key, *args)
      msg = args[0]

      if msg.channel.nil? then
        bug("Empty channel at KICK: %s", msg)
      elsif msg.nick.nil? then
        bug("Empty nick in message at KICK: %s", msg)
      else
        @state.leave(msg.channel, msg.params(1))
      end

      false
    end # def _conn_kick_received

    def _conn_mode_received(ret, key, *args)
      msg = args[0]

      if msg.channel.nil? then
        bug("Empty channel at MODE: %s", msg)
      elsif !@state.channels.include?(msg.channel) then
        bug("Unknown channel at MODE: %s, but %s", msg.channel, @state.channels)
      else
        mode = @state.channel(msg.channel)
        mode.set_mode(*msg.params[1..-1])
      end

      false
    end # def _conn_mode_received

    def _conn_nick_received(ret, key, *args)
      msg = args[0]

      @state.channels.each do |ch|
        mode = @state.channel(ch)
        if mode.joined?(msg.nick) then
          mode.change_nick(msg.nick, msg.params(0))
        end
      end

      false
    end # def _conn_nick_received

    def _conn_part_received(ret, key, *args)
      msg = args[0]

      if msg.channel.nil? then
        bug("Empty channel at PART: %s", msg)
      elsif msg.nick.nil? then
        bug("Empty nick in message at PART: %s", msg)
      else
        @state.leave(msg.channel, msg.nick)
      end

      false
    end # def _conn_part_received

    def _conn_ping_received(ret, key, *args)
      msg = args[0]
      t = Time.now

      unless @value.has_key?(:ping) then
        @value[:ping] = PRUNE::TYPE::PingInfoStruct.new(0, t - 301, nil)
      end
      if !msg.params(1).nil? && !msg.params(1).empty? then
        if msg.params(1) == @state.nick then
          ret.emit_message(key, :Sent, false, PRUNE::Message::PONG.new(msg.params(1), msg.params(0)))
          @value[:ping].pinged_time = nil
          @value[:ping].timer = t
          @value[:ping].signature = nil
          true
        else
          false
        end
      else
        ret.emit_message(key, :Sent, false, PRUNE::Message::PONG.new(msg.params(0)))
        @value[:ping].pinged_time = nil
        @value[:ping].timer = t
        @value[:ping].signature = nil
        true
      end
    end # def _conn_ping_received

    def _conn_pong_received(ret, key, *args)
      msg = args[0]
      t = Time.now

      unless @value.has_key?(:ping) then
        @value[:ping] = PRUNE::TYPE::PingInfoStruct.new(0, t - 301, nil)
      end
      if !msg.params(1).nil? && !msg.params(1).empty? then
        if msg.params(1) == @value[:ping].signature then
          @value[:ping].pinged_time = nil
          @value[:ping].timer = t
          @value[:ping].signature = nil
          true
        else
          false
        end
      else
        if msg.params(0) == @value[:ping].signature then
          @value[:ping].pinged_time = nil
          @value[:ping].timer = t
          @value[:ping].signature = nil
          true
        else
          false
        end
      end
    end # def _conn_pong_received

    def _conn_quit_received(ret, key, *args)
      msg = args[0]

      if msg.nick.nil? then
        bug("Empty nick in message at QUIT: %s", msg)
      else
        @state.quit(msg.nick)
      end

      false
    end # def _conn_quit_received

    def _conn_topic_received(ret, key, *args)
      msg = args[0]

      if msg.channel.nil? then
        bug("Empty channel at TOPIC: %s", msg)
      elsif !@state.channels.include?(msg.channel) then
        bug("Unknown channel at TOPIC: %s, but %s", msg.channel, @state.channels)
      else
        mode = @state.channel(msg.channel)
        mode.topic = msg.params(1)
      end

      false
    end # def _conn_topic_received

    def _conn_001_received(ret, key, *args)
      ret.emit(key, :Loggedin, nil)

      false
    end # def _conn_001_received

    # RPL_CHANNELMODEIS
    def _conn_324_received(ret, key, *args)
      msg = args[0]

      if msg.channel.nil? then
        bug("Empty channel at RPL_CHANNELMODEIS: %s", msg)
      elsif !@state.channels.include?(msg.channel) then
        bug("Unknown channel at RPL_CHANNELMODEIS: %s, but %s", msg.channel, @state.channels)
      else
        mode = @state.channel(msg.channel)
        mode.clear_mode
        mode.set_mode(*msg.params[2..-1])
      end

      false
    end # def _conn_324_received

    # RPL_TOPIC
    def _conn_332_received(ret, key, *args)
      msg = args[0]

      if msg.channel.nil? then
        bug("Empty channel at RPL_TOPIC: %s", msg)
      elsif !@state.channels.include?(msg.channel) then
        bug("Unknown channel at RPL_TOPIC: %s, but %s", msg.channel, @state.channels)
      else
        mode = @state.channel(msg.channel)
        mode.topic = msg.params(2)
      end

      false
    end # def _conn_332_received

    # RPL_NAMREPLY
    def _conn_353_received(ret, key, *args)
      msg = args[0]

      if msg.channel.nil? then
        bug("Empty channel at RPL_NAMREPLY: %s", msg)
      elsif !msg.params(-1).gsub(/[@+]/, '').split(' ').include?(@state.nick) then
        # this may be the result of NAMES. and not relevant to you at all.
      elsif !@state.channels.include?(msg.channel) then
        bug("Unknown channel at RPL_NAMREPLY: %s, but %s", msg.channel, @state.channels)
      else
        mode = @state.channel(msg.channel)
        # to be safe
        mode.clear_oper
        mode.clear_voice

        msg.params(-1).split(' ').each do |n|
          nick = n.sub(/^[@+]/, '')
          unless mode.joined?(nick) then
            mode.join(nick)
          end
          if n =~ /\A@/ then #
            mode.set_oper(nick, true)
          end
          if n =~ /\A\+/ then #
            mode.set_voice(nick, true)
          end
        end
      end

      false
    end # def _conn_353_received

    # RPL_BANLIST
    def _conn_367_received(ret, key, *args)
      msg = args[0]
      mask = msg.params(2)

      if msg.channel.nil? then
        bug("Empty channel at RPL_BANLIST: %s", msg)
      elsif !@state.channels.include?(msg.channel) then
        bug("Unknown channel at RPL_BANLIST: %s, but %s", msg.channel, @state.channels)
      else
        mode = @state.channel(msg.channel)
        mode.set_mode("+b", mask)
      end

      false
    end # def _conn_367_received

    # ERR_NICKNAMEINUSE
    def _conn_433_received(ret, key, *args)
      msg = args[0]

      unless @state.loggedin? then
        if @conninfo.options.has_key?('Nicks') &&
            !@conninfo.options['Nicks'].empty? then
          @conninfo.options['Nicks'] << @conninfo.nick
          @state.nick = @conninfo.nick = @conninfo.options['Nicks'].shift
          ret.emit_message(key, :Sent, false, PRUNE::Message::NICK.new(@state.nick))

          true
        else
          warning("No alternative nicks anymore.")

          false
        end
      else
        false
      end
    end # def _conn_433_received

  end # class IRCConnector

end # module PRUNE
